// SPDX-FileCopyrightText: Adam Evyčędo
//
// SPDX-License-Identifier: GPL-3.0-or-later

package xyz.apiote.bimba.czwek.api

import android.os.Parcelable
import android.util.Log
import kotlinx.parcelize.Parcelize
import org.yaml.snakeyaml.Yaml
import xyz.apiote.bimba.czwek.api.structs.VehicleStatusV1
import xyz.apiote.fruchtfleisch.Reader
import java.io.InputStream
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
import java.time.ZoneId
import java.time.ZonedDateTime
import kotlin.reflect.KClass

class TrafficFormatException(override val message: String) : IllegalArgumentException()
class UnknownResourceVersionException(resource: String, val version: ULong) :
	Exception("unknown version $version of $resource")

class UnknownResourceException(resource: String, cls: KClass<*>) :
	Exception("unknown class $cls for $resource")

data class BimbaInfo(
	val contact: Map<String, String>,
)

data class Bimba(
	val info: BimbaInfo,
	val servers: List<Map<String, String>>,
	val security: List<Map<String, List<*>>>
) {
	companion object {
		@Suppress("UNCHECKED_CAST")
		fun unmarshal(stream: InputStream): Bimba {
			val map = Yaml().load(stream) as HashMap<String, *>
			val contact =
				if (map["info"] is Map<*, *> && (map["info"] as Map<*, *>).keys.all { it is String }) {
					(map["info"] as Map<String, *>)["contact"]
				} else {
					throw TrafficFormatException("invalid info format")
				}
			val contactMap =
				if (contact is Map<*, *> && contact.all { it.key is String && it.value is String }) {
					contact as Map<String, String>
				} else {
					throw TrafficFormatException("invalid contact format")
				}
			val servers = if (map["servers"] is List<*> && (map["servers"] as List<*>).all { server ->
					server is Map<*, *> && server.all { it.key is String && it.value is String }
				}) {
				map["servers"] as List<Map<String, String>>
			} else {
				throw TrafficFormatException("invalid servers format")
			}
			val security =
				if (map["security"] is List<*> && (map["security"] as List<*>).all { security ->
						security is Map<*, *> && security.all { it.key is String && it.value is List<*> }
					}) {
					map["security"] as List<Map<String, List<*>>>
				} else {
					throw TrafficFormatException("invalid security format")
				}
			val bimba = Bimba(BimbaInfo(contactMap), servers, security)
			bimba.validate()
			return bimba
		}
	}

	fun isPrivate(): Boolean {
		return security.size == 1 && security[0]["api_key"] != null
	}

	fun isRateLimited(): Boolean {
		val items = security.foldRight(0b00) { map, acc ->
			acc or when {
				map.containsKey("api_key") -> 0b10
				map.isEmpty() -> 0b01
				else -> 0b00
			}
		}
		Log.i("Rate limited", "${this}, $items")
		return security.size == 2 && items == 0b11
	}

	private fun validate() {
		if (servers.isEmpty() || servers[0]["url"] == null) {
			throw TrafficFormatException("no server in info")
		}
		if (security.isEmpty()) {
			throw TrafficFormatException("no security")
		}
	}
}

@Parcelize
data class PositionV1(
	val latitude: Double, val longitude: Double
) : Parcelable {

	override fun toString(): String = "$latitude,$longitude"


	companion object {
		fun unmarshal(stream: InputStream): PositionV1 {
			val reader = Reader(stream)
			return PositionV1(
				reader.readFloat64(), reader.readFloat64()
			)
		}
	}
}


enum class AlertCauseV1 {
	UNKNOWN, OTHER, TECHNICAL_PROBLEM, STRIKE, DEMONSTRATION, ACCIDENT, HOLIDAY, WEATHER, MAINTENANCE,
	CONSTRUCTION, POLICE_ACTIVITY, MEDICAL_EMERGENCY;

	companion object {
		fun of(type: UInt): AlertCauseV1 {
			return when (type) {
				0u -> valueOf("UNKNOWN")
				1u -> valueOf("OTHER")
				2u -> valueOf("TECHNICAL_PROBLEM")
				3u -> valueOf("STRIKE")
				4u -> valueOf("DEMONSTRATION")
				5u -> valueOf("ACCIDENT")
				6u -> valueOf("HOLIDAY")
				7u -> valueOf("WEATHER")
				8u -> valueOf("MAINTENANCE")
				9u -> valueOf("CONSTRUCTION")
				10u -> valueOf("POLICE_ACTIVITY")
				11u -> valueOf("MEDICAL_EMERGENCY")
				else -> throw UnknownResourceVersionException("AlertCause/$type", 1u)
			}
		}
	}
}

enum class AlertEffectV1 {
	UNKNOWN, OTHER, NO_SERVICE, REDUCED_SERVICE, SIGNIFICANT_DELAYS, DETOUR, ADDITIONAL_SERVICE,
	MODIFIED_SERVICE, STOP_MOVED, NONE, ACCESSIBILITY_ISSUE;

	companion object {
		fun of(type: UInt): AlertEffectV1 {
			return when (type) {
				0u -> valueOf("UNKNOWN")
				1u -> valueOf("OTHER")
				2u -> valueOf("NO_SERVICE")
				3u -> valueOf("REDUCED_SERVICE")
				4u -> valueOf("SIGNIFICANT_DELAYS")
				5u -> valueOf("DETOUR")
				6u -> valueOf("ADDITIONAL_SERVICE")
				7u -> valueOf("MODIFIED_SERVICE")
				8u -> valueOf("STOP_MOVED")
				9u -> valueOf("NONE")
				10u -> valueOf("ACCESSIBILITY_ISSUE")
				else -> throw UnknownResourceVersionException("AlertEffect/$type", 1u)
			}
		}
	}
}

data class AlertV1(
	val header: String,
	val Description: String,
	val Url: String,
	val Cause: AlertCauseV1,
	val Effect: AlertEffectV1
) {
	companion object {
		fun unmarshal(stream: InputStream): AlertV1 {
			val reader = Reader(stream)
			return AlertV1(
				reader.readString(),
				reader.readString(),
				reader.readString(),
				AlertCauseV1.of(reader.readUInt().toULong().toUInt()),
				AlertEffectV1.of(reader.readUInt().toULong().toUInt())
			)
		}
	}
}

data class Time(
	val Hour: UInt, val Minute: UInt, val Second: UInt, val DayOffset: Byte, val Zone: String
) {
	companion object {
		fun unmarshal(stream: InputStream): Time {
			val reader = Reader(stream)
			return Time(
				reader.readUInt().toULong().toUInt(),
				reader.readUInt().toULong().toUInt(),
				reader.readUInt().toULong().toUInt(),
				reader.readI8(),
				reader.readString()
			)
		}
	}

	fun toDateTime(): ZonedDateTime {
		return ZonedDateTime.of(
			LocalDateTime.of(
				LocalDate.now().plusDays(DayOffset.toLong()),
				LocalTime.of(Hour.toInt(), Minute.toInt(), Second.toInt())
			),
			ZoneId.of(Zone)
		)
	}
}

data class ColourV1(val R: UByte, val G: UByte, val B: UByte) {
	companion object {
		fun unmarshal(stream: InputStream): ColourV1 {
			val reader = Reader(stream)
			return ColourV1(
				reader.readU8(), reader.readU8(), reader.readU8()
			)
		}
	}
}

enum class CongestionLevelV1 {
	UNKNOWN, SMOOTH, STOP_AND_GO, SIGNIFICANT, SEVERE;

	companion object {
		fun of(type: UInt): CongestionLevelV1 {
			return when (type) {
				0u -> valueOf("UNKNOWN")
				1u -> valueOf("SMOOTH")
				2u -> valueOf("STOP_AND_GO")
				3u -> valueOf("SIGNIFICANT")
				4u -> valueOf("SEVERE")
				else -> throw UnknownResourceVersionException("CongestionLevel/$type", 1u)
			}
		}
	}
}

enum class OccupancyStatusV1 {
	UNKNOWN, EMPTY, MANY_AVAILABLE, FEW_AVAILABLE, STANDING_ONLY, CRUSHED, FULL, NOT_ACCEPTING;

	companion object {
		fun of(type: UInt): OccupancyStatusV1 {
			return when (type) {
				0u -> valueOf("UNKNOWN")
				1u -> valueOf("EMPTY")
				2u -> valueOf("MANY_AVAILABLE")
				3u -> valueOf("FEW_AVAILABLE")
				4u -> valueOf("STANDING_ONLY")
				5u -> valueOf("CRUSHED")
				6u -> valueOf("FULL")
				7u -> valueOf("NOT_ACCEPTING")
				else -> throw UnknownResourceVersionException("OccupancyStatus/$type", 1u)
			}
		}
	}
}

data class VehicleV1(
	val ID: String,
	val Position: PositionV1,
	val Capabilities: UShort,
	val Speed: Float,
	val Line: LineStubV1,
	val Headsign: String,
	val CongestionLevel: CongestionLevelV1,
	val OccupancyStatus: OccupancyStatusV1
) : LocatableV1 {
	companion object {
		fun unmarshal(stream: InputStream): VehicleV1 {
			val reader = Reader(stream)
			return VehicleV1(
				reader.readString(),
				PositionV1.unmarshal(stream),
				reader.readU16(),
				reader.readFloat32(),
				LineStubV1.unmarshal(stream),
				reader.readString(),
				CongestionLevelV1.of(reader.readUInt().toULong().toUInt()),
				OccupancyStatusV1.of(reader.readUInt().toULong().toUInt())
			)
		}
	}
}

data class VehicleV2(
	val ID: String,
	val Position: PositionV1,
	val Capabilities: UShort,
	val Speed: Float,
	val Line: LineStubV2,
	val Headsign: String,
	val CongestionLevel: CongestionLevelV1,
	val OccupancyStatus: OccupancyStatusV1
) : LocatableV2 {
	companion object {
		fun unmarshal(stream: InputStream): VehicleV2 {
			val reader = Reader(stream)
			return VehicleV2(
				reader.readString(),
				PositionV1.unmarshal(stream),
				reader.readU16(),
				reader.readFloat32(),
				LineStubV2.unmarshal(stream),
				reader.readString(),
				CongestionLevelV1.of(reader.readUInt().toULong().toUInt()),
				OccupancyStatusV1.of(reader.readUInt().toULong().toUInt())
			)
		}
	}
}

data class VehicleV3(
	val ID: String,
	val Position: PositionV1,
	val Capabilities: UShort,
	val Speed: Float,
	val Line: LineStubV3,
	val Headsign: String,
	val CongestionLevel: CongestionLevelV1,
	val OccupancyStatus: OccupancyStatusV1
) : LocatableV3 {
	companion object {
		fun unmarshal(stream: InputStream): VehicleV3 {
			val reader = Reader(stream)
			return VehicleV3(
				reader.readString(),
				PositionV1.unmarshal(stream),
				reader.readU16(),
				reader.readFloat32(),
				LineStubV3.unmarshal(stream),
				reader.readString(),
				CongestionLevelV1.of(reader.readUInt().toULong().toUInt()),
				OccupancyStatusV1.of(reader.readUInt().toULong().toUInt())
			)
		}
	}
}

data class LineStubV1(
	val name: String, val kind: LineTypeV1, val colour: ColourV1
) {
	companion object {
		fun unmarshal(stream: InputStream): LineStubV1 {
			val reader = Reader(stream)
			return LineStubV1(
				reader.readString(),
				LineTypeV1.of(reader.readUInt().toULong().toUInt()),
				ColourV1.unmarshal(stream)
			)
		}
	}
}

data class LineStubV2(
	val name: String, val kind: LineTypeV2, val colour: ColourV1
) {
	companion object {
		fun unmarshal(stream: InputStream): LineStubV2 {
			val reader = Reader(stream)
			return LineStubV2(
				reader.readString(),
				LineTypeV2.of(reader.readUInt().toULong().toUInt()),
				ColourV1.unmarshal(stream)
			)
		}
	}
}

data class LineStubV3(
	val name: String, val kind: LineTypeV3, val colour: ColourV1
) {
	companion object {
		fun unmarshal(stream: InputStream): LineStubV3 {
			val reader = Reader(stream)
			return LineStubV3(
				reader.readString(),
				LineTypeV3.of(reader.readUInt().toULong().toUInt()),
				ColourV1.unmarshal(stream)
			)
		}
	}
}

data class DepartureV1(
	val ID: String,
	val time: Time,
	val status: ULong,
	val isRealtime: Boolean,
	val vehicle: VehicleV1,
	val boarding: UByte
) {

	companion object {
		fun unmarshal(stream: InputStream): DepartureV1 {
			val reader = Reader(stream)
			val id = reader.readString()
			val time = Time.unmarshal(stream)
			val status = reader.readUInt().toULong()
			val isRealtime = reader.readBoolean()
			val vehicle = VehicleV1.unmarshal(stream)
			val boarding = reader.readU8()
			return DepartureV1(id, time, status, isRealtime, vehicle, boarding)
		}
	}
}

data class DepartureV2(
	val ID: String,
	val time: Time,
	val status: ULong,
	val isRealtime: Boolean,
	val vehicle: VehicleV2,
	val boarding: UByte
) {

	companion object {
		fun unmarshal(stream: InputStream): DepartureV2 {
			val reader = Reader(stream)
			val id = reader.readString()
			val time = Time.unmarshal(stream)
			val status = reader.readUInt().toULong()
			val isRealtime = reader.readBoolean()
			val vehicle = VehicleV2.unmarshal(stream)
			val boarding = reader.readU8()
			return DepartureV2(id, time, status, isRealtime, vehicle, boarding)
		}
	}
}

data class DepartureV3(
	val ID: String,
	val time: Time,
	val status: VehicleStatusV1,
	val isRealtime: Boolean,
	val vehicle: VehicleV3,
	val boarding: UByte
) {

	companion object {
		fun unmarshal(stream: InputStream): DepartureV3 {
			val reader = Reader(stream)
			val id = reader.readString()
			val time = Time.unmarshal(stream)
			val status = VehicleStatusV1.of(reader.readUInt().toULong().toUInt())
			val isRealtime = reader.readBoolean()
			val vehicle = VehicleV3.unmarshal(stream)
			val boarding = reader.readU8()
			return DepartureV3(id, time, status, isRealtime, vehicle, boarding)
		}
	}
}

@Parcelize
data class StopV2(
	val code: String,
	val name: String,
	val nodeName: String,
	val zone: String,
	val feedID: String,
	val position: PositionV1,
	val changeOptions: List<ChangeOptionV1>
) : QueryableV2, Parcelable, LocatableV2, QueryableV3, LocatableV3 {
	companion object {
		fun unmarshal(stream: InputStream): StopV2 {
			val reader = Reader(stream)
			val code = reader.readString()
			val name = reader.readString()
			val nodeName = reader.readString()
			val zone = reader.readString()
			val feedID = reader.readString()
			val position = PositionV1.unmarshal(stream)
			val chOptionsNum = reader.readUInt().toULong()
			val changeOptions = mutableListOf<ChangeOptionV1>()
			for (i in 0UL until chOptionsNum) {
				changeOptions.add(ChangeOptionV1.unmarshal(stream))
			}
			return StopV2(
				name = name,
				nodeName = nodeName,
				code = code,
				zone = zone,
				position = position,
				feedID = feedID,
				changeOptions = changeOptions
			)
		}
	}
}

@Parcelize
data class StopV1(
	val code: String,
	val name: String,
	val zone: String,
	val position: PositionV1,
	val changeOptions: List<ChangeOptionV1>
) : QueryableV1, Parcelable, LocatableV1 {
	companion object {
		fun unmarshal(stream: InputStream): StopV1 {
			val reader = Reader(stream)
			val code = reader.readString()
			val name = reader.readString()
			val zone = reader.readString()
			val position = PositionV1.unmarshal(stream)
			val chOptionsNum = reader.readUInt().toULong()
			val changeOptions = mutableListOf<ChangeOptionV1>()
			for (i in 0UL until chOptionsNum) {
				changeOptions.add(ChangeOptionV1.unmarshal(stream))
			}
			return StopV1(
				name = name, code = code, zone = zone, position = position, changeOptions = changeOptions
			)
		}
	}
}

data class LineV1(
	val name: String,
	val colour: ColourV1,
	val type: LineTypeV2,
	val feedID: String,
	val headsigns: List<List<String>>,
	val graphs: List<LineGraph>,
) : QueryableV2 {
	override fun toString(): String {
		return "$name ($type) [$colour]\n${headsigns.map { "-> ${it.joinToString()}" }}"
	}

	companion object {
		fun unmarshal(stream: InputStream): LineV1 {
			val reader = Reader(stream)
			val name = reader.readString()
			val colour = ColourV1.unmarshal(stream)
			val type = reader.readUInt()
			val feedID = reader.readString()
			var directionsNum = reader.readUInt().toULong()
			val headsigns = (0UL until directionsNum).map {
				val headsignsNum = reader.readUInt().toULong()
				val headsignsDir = mutableListOf<String>()
				for (j in 0UL until headsignsNum) {
					headsignsDir.add(reader.readString())
				}
				headsignsDir
			}
			directionsNum = reader.readUInt().toULong()
			val graphs = mutableListOf<LineGraph>()
			for (i in 0UL until directionsNum) {
				graphs.add(LineGraph.unmarshal(stream))
			}
			return LineV1(
				name = name,
				colour = colour,
				type = LineTypeV2.of(type.toULong().toUInt()),
				feedID = feedID,
				headsigns = headsigns,
				graphs = graphs
			)
		}
	}
}

data class LineV2(
	val name: String,
	val colour: ColourV1,
	val type: LineTypeV3,
	val feedID: String,
	val headsigns: List<List<String>>,
	val graphs: List<LineGraph>,
) : QueryableV3 {
	override fun toString(): String {
		return "$name ($type) [$colour]\n${headsigns.map { "-> ${it.joinToString()}" }}"
	}

	companion object {
		fun unmarshal(stream: InputStream): LineV2 {
			val reader = Reader(stream)
			val name = reader.readString()
			val colour = ColourV1.unmarshal(stream)
			val type = reader.readUInt()
			val feedID = reader.readString()
			var directionsNum = reader.readUInt().toULong()
			val headsigns = (0UL until directionsNum).map {
				val headsignsNum = reader.readUInt().toULong()
				val headsignsDir = mutableListOf<String>()
				for (j in 0UL until headsignsNum) {
					headsignsDir.add(reader.readString())
				}
				headsignsDir
			}
			directionsNum = reader.readUInt().toULong()
			val graphs = mutableListOf<LineGraph>()
			for (i in 0UL until directionsNum) {
				graphs.add(LineGraph.unmarshal(stream))
			}
			return LineV2(
				name = name,
				colour = colour,
				type = LineTypeV3.of(type.toULong().toUInt()),
				feedID = feedID,
				headsigns = headsigns,
				graphs = graphs
			)
		}
	}
}

enum class LineTypeV1 {
	UNKNOWN, TRAM, BUS;

	companion object {
		fun of(type: UInt): LineTypeV1 {
			return when (type) {
				0u -> valueOf("UNKNOWN")
				1u -> valueOf("TRAM")
				2u -> valueOf("BUS")
				else -> throw UnknownResourceVersionException("LineType/$type", 1u)
			}
		}
	}
}

enum class LineTypeV2 {
	UNKNOWN, TRAM, BUS, TROLLEYBUS;

	companion object {
		fun of(type: UInt): LineTypeV2 {
			return when (type) {
				0u -> valueOf("UNKNOWN")
				1u -> valueOf("TRAM")
				2u -> valueOf("BUS")
				3u -> valueOf("TROLLEYBUS")
				else -> throw UnknownResourceVersionException("LineType/$type", 2u)
			}
		}
	}
}

enum class LineTypeV3 {
	UNKNOWN, TRAM, BUS, TROLLEYBUS, METRO, RAIL, FERRY, CABLE_TRAM, CABLE_CAR, FUNICULAR, MONORAIL;

	companion object {
		fun of(type: UInt): LineTypeV3 {
			return when (type) {
				0u -> valueOf("UNKNOWN")
				1u -> valueOf("TRAM")
				2u -> valueOf("BUS")
				3u -> valueOf("TROLLEYBUS")
				4u -> valueOf("METRO")
				5u -> valueOf("RAIL")
				6u -> valueOf("FERRY")
				7u -> valueOf("CABLE_TRAM")
				8u -> valueOf("CABLE_CAR")
				9u -> valueOf("FUNICULAR")
				10u -> valueOf("MONORAIL")
				else -> throw UnknownResourceVersionException("LineType/$type", 3u)
			}
		}
	}
}

@Parcelize
data class ChangeOptionV1(val line: String, val headsign: String) : Parcelable {
	companion object {
		fun unmarshal(stream: InputStream): ChangeOptionV1 {
			val reader = Reader(stream)
			return ChangeOptionV1(line = reader.readString(), headsign = reader.readString())
		}
	}
}

data class LineGraph(
	val stops: List<StopStub>, val nextNodes: Map<Long, List<Long>>
) {
	companion object {
		fun unmarshal(stream: InputStream): LineGraph {
			val reader = Reader(stream)
			val stopsNum = reader.readUInt().toULong()
			val stops = mutableListOf<StopStub>()
			for (i in 0UL until stopsNum) {
				stops.add(StopStub.unmarshal(stream))
			}
			val nextNodesNum = reader.readUInt().toULong()
			val nextNodes = mutableMapOf<Long, List<Long>>()
			for (i in 0UL until nextNodesNum) {
				val from = reader.readInt().toLong()
				val toNum = reader.readUInt().toULong()
				val to = mutableListOf<Long>()
				for (j in 0UL until toNum) {
					to.add(reader.readInt().toLong())
				}
				nextNodes[from] = to
			}

			return LineGraph(stops = stops, nextNodes = nextNodes)
		}
	}
}

data class StopStub(
	val code: String, val name: String, val nodeName: String, val zone: String, val onDemand: Boolean
) {
	companion object {
		fun unmarshal(stream: InputStream): StopStub {
			val reader = Reader(stream)
			return StopStub(
				code = reader.readString(),
				name = reader.readString(),
				nodeName = reader.readString(),
				zone = reader.readString(),
				onDemand = reader.readBoolean()
			)
		}
	}
}
